package freemarker.core;

import java.io.Reader;
import java.io.StringReader;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.io.IOUtils;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.springframework.stereotype.Service;

import com.espirit.eap.manager.I18n;
import com.espirit.eap.manager.Taglib;
import com.espirit.eap.pagelayout.BlockSection;
import com.espirit.eap.pagelayout.Element;
import com.espirit.eap.pagelayout.HtmlPageParser;
import com.espirit.eap.pagelayout.Layout;
import com.espirit.eap.pagelayout.SectionElement;
import com.espirit.eap.pagelayout.TagCategory;
import com.espirit.eap.pagelayout.TagInfo;
import com.espirit.eap.pagelayout.TaglibImport;
import com.espirit.eap.util.TaglibManager;
import com.espirit.eap.util.TxFileService;
import com.espirit.eap.util.tagAdaptor.TagAdaptorUtil;
import com.espirit.eap.util.tagAdaptor.TagElementAdaptor;
import com.googlecode.cswish.struts.spring.ObjectFactory;

import freemarker.template.Configuration;
import freemarker.template.Template;

@Service
public final class FreemarkerTemplateParser {

	private static final Set<String> FUNCTION_TYPES;

	private static final Set<String> IGNORE_TAGS;

	// Use TagAdaptorUtil as the logger check flag, keep the same setting with espirit-core project
	private final static Logger logger = Logger.getLogger(TagAdaptorUtil.class);

	private static FreemarkerTemplateParser instance;
	
	private TagAdaptorUtil tagAdaptorUtil;
	
	private TaglibManager taglibManager;
	
	private TxFileService txFileService;

	private FreemarkerTemplateParser(){
		super();
		tagAdaptorUtil = ObjectFactory.getInstance().getBean(TagAdaptorUtil.class, false);
		taglibManager = ObjectFactory.getInstance().getBean(TaglibManager.class, false);
		txFileService = ObjectFactory.getInstance().getBean(TxFileService.class, false);
	}
	
	public static FreemarkerTemplateParser getInstance(){
		if (instance == null) {
			synchronized (HtmlPageParser.class) {
				if (instance == null) {
					instance = new FreemarkerTemplateParser();
				}
			}
		}
		return instance;
	}

	public List<Element> parse(Layout root, String content, Map<String, I18n> i18nMap, boolean unwrap) {
		// use FMParser to parser the template
		Template template = null;
		FMParser parser = null;
		Reader reader = null;
		Reader tempReader = null;
		Element mockRootElement = new Element();
		try {
			tempReader = new StringReader("");
			reader = new StringReader(content);
			template = new Template("", tempReader, new Configuration());
			parser = new FMParser(template, reader, true, true);
			TemplateElement node = parser.Root();
			parse(null, node, root, mockRootElement, i18nMap, unwrap);
		} catch (Exception e) {
			logger.error("Fail to parse template :", e);
		} finally {
			IOUtils.closeQuietly(reader);
		}
		return mockRootElement.getChildren();
	}

	public boolean parse(Enumeration<?> children, Object node, Layout root, Element parent, Map<String, I18n> i18nMap, boolean unwrap) throws Exception {
		Element element = null;
		boolean containsValidTag = true;
		if (node instanceof Include) {
			Include include = (Include) node;
			String name = StringUtils.uncapitalize(include.getNodeName());
			Object value = getFieldValue(Include.class, include, "includedTemplateName");
			String path = getValueAsString(value, true);
			if (unwrap) {
				// load the tag category from cache
				TagCategory tagCategory =  taglibManager.findTagCategoryByPath(path);
				if (tagCategory != null) {
					TaglibImport taglibImport = new TaglibImport();
					taglibImport.setTagCategory(tagCategory);
					taglibImport.setCategory(tagCategory.getName());
					taglibImport.setUseImport(Boolean.FALSE);
					root.safeTaglibImports().add(taglibImport);
				}
				return false;
			} else {
				element = new SectionElement();
				element.setName(name);
				// element.setTagName(name); // it's not standard macro, don't set the tag name;
				element.addExProperty("url", path);
				parent.addChild(element);
			}
		} else if (node instanceof LibraryLoad) {
			LibraryLoad libraryLoad = (LibraryLoad)node;
			// the import tag can rename the category
			String category = (String)getFieldValue(LibraryLoad.class, libraryLoad, "namespace");
			String templateName = libraryLoad.getTemplateName();
			if (templateName.length() >= 2 && templateName.charAt(0) == '"'
					&& templateName.charAt(templateName.length() - 1) == '"') {
				templateName = templateName.substring(1, templateName.length() - 1);	// remove the '""'
			} else {
				// TODO: support the variable?
				logger.info("Don't support to use the variable as template at present");
			}
			// load the taglib by path here
			TagCategory tagCategory = taglibManager.findTagCategoryByPath(templateName);
			TaglibImport taglibImport = new TaglibImport();
			taglibImport.setTagCategory(tagCategory);
			taglibImport.setUseImport(Boolean.TRUE);
			root.safeTaglibImports().add(taglibImport);
			return false;
			
		} else if (node instanceof Macro) {
			Macro macro = (Macro) node;
			if (macro.isFunction()) { // ignore function
				containsValidTag = parseVirtualElement(children, macro, root, parent, i18nMap, unwrap);
			} else {
				String name = macro.getName();
				Map<?, ?> args = (Map<?, ?>) getFieldValue(Macro.class, macro, "args");
				containsValidTag = newElement(children, macro, name, args, root, parent, i18nMap, unwrap);
			}
		} else if (node instanceof UnifiedCall) {
			UnifiedCall uc = (UnifiedCall) node;
			String name = String.valueOf(getFieldValue(UnifiedCall.class, uc, "nameExp"));
			Map<?, ?> args = (Map<?, ?>) getFieldValue(UnifiedCall.class, uc, "namedArgs");
			containsValidTag = newElement(children, uc, name, args, root, parent, i18nMap, unwrap);
		} else if (node instanceof TextBlock) {
			TextBlock tb = (TextBlock) node;
			char[] chars = (char[]) getFieldValue(TextBlock.class, tb, "text");
			String text = new String(chars);
			parseText(text, children, root, parent, i18nMap, unwrap); 
		} else if (node instanceof TemplateElement) {
			TemplateElement te = (TemplateElement) node;
			if (isFunction(te)) {
				// for example:
				// case 1: <#if ((execute.data)![])?size == 0><@i18n name='commons.noRecord' textModel=false/></#if>
				containsValidTag = parseVirtualElement(children, te, root, parent, i18nMap, unwrap);
			} else {
				parseChildren(te, root, parent, i18nMap, unwrap);
			}
		}
		return containsValidTag;
	}

	private void parseText(String text, Enumeration<?> children,
			Layout root, Element parent, Map<String, I18n> i18nMap, boolean unwrap)
			throws Exception {
		String trimText = text.trim();
		if (!trimText.isEmpty()) {
			HtmlPageParser htmlParser = HtmlPageParser.getInstance();
			List<Element> elements = htmlParser.parse(children, root, text, i18nMap, unwrap);
			if (null != elements) {
				// enter the code&template parser model
				// scan the next children and find the close tag of 'code' or 'template'
				for (Element ele : elements) {
					parent.addChild(ele);
					fixTemplateOrCodeTag(text, children, root, parent, ele, i18nMap, unwrap);
				}
			} else {	// elements is null
				// treat the whole text as a virtual node
				// TODO: generate the name, use TagAdaptorUtil.createName(layout, 'code')?
				addVirtualTag(null, text, parent);
			}
		}
	}

	/**
	 * The template tag and code tag has the special logic, parse it again  
	 * 
	 * @param text
	 * @param children
	 * @param parent
	 * @param i18nMap
	 * @param unwrap
	 * @throws Exception
	 */
	private void fixTemplateOrCodeTag(String text, Enumeration<?> children,
			Layout root, Element parent, Element element, Map<String, I18n> i18nMap, boolean unwrap) throws Exception {
		if (TagElementAdaptor.TAG_CODE.equals(element.getTagName()) ||
				TagElementAdaptor.TAG_TEMPLATE.equals(element.getTagName())) {
			if (children != null && children.hasMoreElements()) {
				String tagName = getFirstTagName(text);
				String openTagFlag = "<" + tagName;
				String closeTagFlag = "</" + tagName + ">";
				
				// TODO: use better way to identify the block: <div uitype='code'>xxx</div>
				StringBuilder sb = new StringBuilder();
				int begin = text.indexOf('>') + 1;

				String unParseText = null;
				int[] tagGap = new int[] {0};		// the count gap between open tag and close tag
				do {
					int endPos = foundCloseTagPos(text, tagGap, openTagFlag, closeTagFlag);
					if (endPos > 0) {
						sb.append(text.substring(begin, endPos));
						unParseText = text.substring(endPos + closeTagFlag.length());
					} else {
						sb.append(text.substring(begin));
						
						// continue to search the sibling, and find the end flag "</div>"
						while (children.hasMoreElements()) {
							Object nextNode = children.nextElement();
							if (nextNode instanceof TextBlock) {
								char[] values = (char[]) getFieldValue(TextBlock.class, nextNode, "text");
								String str = new String(values);
								endPos = foundCloseTagPos(str, tagGap, openTagFlag, closeTagFlag);
								if (endPos > 0) {
									sb.append(str.substring(0, endPos));
									unParseText = str.substring(endPos + closeTagFlag.length());
									break;
								}
							}
							// FIXME: disable the auto format
							sb.append(nextNode);
						}
					}
				} while (tagGap[0] != 0 && children.hasMoreElements());
				
				int start = 0, end = sb.length();
				// TODO: think about it
				/*if (end > 0 && sb.charAt(0) == '\n') {
					start++;
				}
				if (end > 0 && sb.charAt(end - 1) == '\n') {
					end--;
				}*/
				if (TagElementAdaptor.TAG_CODE.equals(element.getTagName())) {
					element.addExProperty("value", sb.substring(start, end));
				} else {
					// template code
					parseTemplate(root, element, sb.substring(start, end), i18nMap);
				}
				
				if (unParseText != null && unParseText.trim().length() > 0) {
					parseText(unParseText, children, root, parent, i18nMap, unwrap);
				}
			}
		}
	}
	
	private int foundCloseTagPos(String content, int[] tagGapCount, String openTag, String closeTag) {
		Pattern pattern = Pattern.compile('(' + openTag + '|' + closeTag + ')');
		Matcher matcher = pattern.matcher(content);
		
		int endPos = -1;
		int gapCount = tagGapCount[0];
		while (matcher.find()) {
			String found = matcher.group(0);
			if (found.equals(openTag)) {
				gapCount++;
			} else {
				gapCount--;
			}
			
			if (gapCount == 0) {
				endPos = matcher.start();
				break;
			}
		}
		
		tagGapCount[0] = gapCount;
		return endPos;
	}
	
	// 反向解析
	private void parseTemplate(Layout root, Element element, String content, Map<String, I18n> i18nMap) {
		// update the new context to template file
		String url = element.getExProperty("url");
		boolean templateContainTagInfo = "true".equals(element.getExProperty(TagElementAdaptor.TAG_TEMPLATE_CONTAIN));
		String tagInfo = element.getExProperty(TagElementAdaptor.TAG_TEMPLATE_TAGINFO);
		
		// format the context: convert it to [var], parse them to elements
		List<Element> childrenElements = parse(root, content, i18nMap, true);
		
		StringBuilder formatContent = new StringBuilder();
		if (templateContainTagInfo) {
			// add the tag information to template file
			// FIXME: path
			formatContent.append(tagInfo.substring(0, tagInfo.lastIndexOf('<')));
		}
		
		for (Element child : childrenElements) {
			String tagName = child.getTagName();
			if (TagElementAdaptor.TAG_CODE.equals(tagName)) {
				String exProperty = child.getExProperty();
				int index = exProperty == null ? -1 : exProperty.indexOf(Element.EXPROP_KV_SEP);
				if (index > 0) {
					int begin = exProperty.indexOf(Element.EXPROP_KV_SEP, index);
					if (begin > 0) {
						String value = exProperty.substring(begin + 1);
						formatContent.append(value);
					}
				}
			} else {
				// convert the element to 'var'
				element.addChild(child);
				child.setParent(element);
				formatContent.append("[var]");
			}
		}
		
		if (templateContainTagInfo) {
			formatContent.append(tagInfo.substring(tagInfo.lastIndexOf('<')));
		}
		txFileService.write(null, url, formatContent.toString());
	}

	private String getFirstTagName(String context) {
		String firstTagName = "div";
		int begin = context.indexOf('<') + 1;
		if (begin > 0) {
			int end = context.indexOf(' ', begin);
			if (end > begin) {
				firstTagName = context.substring(begin, end);
			}
		}
		return firstTagName;
	}

	private boolean newElement(Enumeration<?> children, TemplateElement node, String name, Map<?, ?> args, Layout root, Element parent, Map<String, I18n> i18nMap, boolean unwrap)throws Exception {
		Element element = null;
		boolean containsValidTag = false;
		if ("node".equals(name)) {
			containsValidTag = parseVirtualElement(children, node, root, parent, i18nMap, unwrap);
		}else{
			element = newValidElement(name, args, root, parent, i18nMap, unwrap);
			if (null == element) {
				element = parent;
			}
			containsValidTag = true;
			parseChildren(node, root, element, i18nMap, unwrap);
		}
		
		return containsValidTag;
	}

	private boolean parseVirtualElement(Enumeration<?> children, TemplateElement node, Layout root, Element parent, Map<String, I18n> i18nMap, boolean unwrap) throws Exception {
		TemplateElement nestedBlock = (TemplateElement) getFieldValue(TemplateElement.class, node, "nestedBlock");
		String nodeContent = String.valueOf(invokeMethod(TemplateObject.class, node, "getCanonicalForm"));
		boolean containsValidTag = false;
		if (null != nestedBlock) {
			int length = nodeContent.length();
			if (length < 200) {
				// treat the whole text as virtual node
				addVirtualTag(null, nodeContent, parent);
			} else {
				// use 2 virtual node to wrap the context
				String nestedContent = String.valueOf(invokeMethod(TemplateObject.class, nestedBlock, "getCanonicalForm"));
				String[] nodes = StringUtils.splitByWholeSeparator(nodeContent, nestedContent);
				addVirtualTag(null, nodes[0], parent);
				containsValidTag = parse(children, nestedBlock, root, parent, i18nMap, unwrap);
				addVirtualTag(null, nodes[1], parent);
			}
		} else {
			if (unwrap && "<#assign _topPage=true>".equals(nodeContent)) {		// skip the _topPage
				return false;
			}
			
			String name = null;
			Enumeration enumeration = node.getParent().children();
			Object previousSibling = null;
			while (enumeration.hasMoreElements()) {
				Object element = enumeration.nextElement();
				if (element == node) {
					break;
				}
				previousSibling = element;
			}
			if (previousSibling instanceof TextBlock) {
				char[] chars = (char[]) getFieldValue(TextBlock.class, previousSibling, "text");
				String text = new String(chars);
				HtmlPageParser htmlParser = HtmlPageParser.getInstance();
				List<Element> elements = htmlParser.parse(children, root, text, i18nMap, unwrap);
				if (elements != null && elements.size() > 0) {
					name = elements.get(0).getName();
				}
			}
			addVirtualTag(name, nodeContent, parent);
		}
		return containsValidTag;
	}

	private void addVirtualTag(String name, String content, Element parent) {
		Element element = new SectionElement();
		element.setTagName(TagElementAdaptor.TAG_CODE);
		element.setName(name);
		element.addExProperty("value", content);
		parent.addChild(element);
	}

	private boolean isFunction(TemplateElement node) throws Exception {
		if (node instanceof MixedContent) {
			return false;
		}
		String nodeName = node.getNodeName();
		return FUNCTION_TYPES.contains(nodeName);
	}

	private Object invokeMethod(Class<?> clazz, TemplateElement node, String methodName) throws Exception {
		Method method = clazz.getDeclaredMethod(methodName);
		method.setAccessible(true);
		return method.invoke(node);
	}

	private Element newValidElement(String name, Map<?, ?> args, Layout root, Element parent, Map<String, I18n> i18nMap, boolean unwrap) {
		if (unwrap && IGNORE_TAGS.contains(name)) {
			return null;
		}
		List<String> categories = root.getCategories();
		TagInfo tagInfo = taglibManager.getDefaultTagInfo(categories, name);
		if (tagInfo == null) {
			if (logger.isInfoEnabled()) {
				logger.warn("unknown tag: " + name + ", treat it as SectionElement");
			}
		}
		boolean isContaniner = tagInfo == null ? false : tagInfo.getContainer() != null && tagInfo.getContainer();
		Element element;
		if (isContaniner) {
			element = new BlockSection();
		}else{
			element = new SectionElement();
		}
		element.setTagName(name);
		if (null != args) {
			Map<String,Object> defaultProperties = taglibManager.getDefaultTagProperty(categories, name);
			Map<String,String> properties = new HashMap<>(); 
			String i18nKey =  getValueAsString(args.get("label"), true);
			if (i18nKey != null) {
				element.setI18n(i18nKey);
			}
			for (Map.Entry<?, ?> entry : args.entrySet()) {
				String key = getValueAsString(entry.getKey(), true);
				boolean targetTagPropertyIsString = tagInfo != null && "text".equals(tagInfo.getPropertyType(key));
				String value = getValueAsString(entry.getValue(), targetTagPropertyIsString);
				if (!"label".equals(key)) {
					properties.put(key, value);
				}
			}
			if (isContaniner) {
				tagAdaptorUtil.updateElement(properties, defaultProperties, element);
			} else {
				tagAdaptorUtil.updateElement(properties, defaultProperties,(SectionElement)element);
			}
		}
		parent.addChild(element);
		
		return element;
	}

	private String getValueAsString(Object obj, boolean targetTagPropertyIsString) {
		String str = null;
		if (null == obj) {
			str = null;
		} else if (obj instanceof String) {
			str = (String) obj;
		} else if (obj instanceof StringLiteral) {
			str = ((StringLiteral) obj).getAsString();
		} else if (targetTagPropertyIsString) {
			// see ConversionCallback.appendProperty,  if (str.charAt(0) == '%' ...
			// use ':' to identify the value is expression, and don't wrap the quotation 
			// when generating the context
			str = '%' + String.valueOf(obj);
		} else {
			str = String.valueOf(obj);
		}
		return str;
	}

	private Object getFieldValue(Class<?> clazz, Object obj, String fieldName) throws Exception {
		Field field = clazz.getDeclaredField(fieldName);
		field.setAccessible(true);
		return field.get(obj);
	}

	private void parseChildren(Object node, Layout root, Element parent, Map<String, I18n> i18nMap, boolean unwrap) throws Exception {
		TemplateElement te = (TemplateElement) node;
		if (te.getAllowsChildren()) {
			Enumeration<?> children = te.children();
			while (children.hasMoreElements()) {
				Object child = children.nextElement();
				parse(children, child, root, parent, i18nMap, unwrap);
			}
		}
	}

	static {
		FUNCTION_TYPES = new HashSet<>();
		FUNCTION_TYPES.add(Assignment.class.getSimpleName());
		FUNCTION_TYPES.add(AssignmentInstruction.class.getSimpleName());
		FUNCTION_TYPES.add(AttemptBlock.class.getSimpleName());
		FUNCTION_TYPES.add(BlockAssignment.class.getSimpleName());
		FUNCTION_TYPES.add(BodyInstruction.class.getSimpleName());
		FUNCTION_TYPES.add(BreakInstruction.class.getSimpleName());
		FUNCTION_TYPES.add(Case.class.getSimpleName());
		FUNCTION_TYPES.add(CompressedBlock.class.getSimpleName());
		FUNCTION_TYPES.add(ConditionalBlock.class.getSimpleName());
		FUNCTION_TYPES.add(EscapeBlock.class.getSimpleName());
		FUNCTION_TYPES.add(FallbackInstruction.class.getSimpleName());
		FUNCTION_TYPES.add(IfBlock.class.getSimpleName());
		FUNCTION_TYPES.add(IteratorBlock.class.getSimpleName());
		FUNCTION_TYPES.add(LibraryLoad.class.getSimpleName());
		FUNCTION_TYPES.add(NoEscapeBlock.class.getSimpleName());
		FUNCTION_TYPES.add(PropertySetting.class.getSimpleName());
		FUNCTION_TYPES.add(RecoveryBlock.class.getSimpleName());
		FUNCTION_TYPES.add(ReturnInstruction.class.getSimpleName());
		FUNCTION_TYPES.add(StopInstruction.class.getSimpleName());
		FUNCTION_TYPES.add(SwitchBlock.class.getSimpleName());
		FUNCTION_TYPES.add(TransformBlock.class.getSimpleName());
		FUNCTION_TYPES.add(TrimInstruction.class.getSimpleName());
		FUNCTION_TYPES.add(VisitNode.class.getSimpleName());
		FUNCTION_TYPES.add(DollarVariable.class.getSimpleName());

		IGNORE_TAGS = new HashSet<>();
		IGNORE_TAGS.add("form");
		//IGNORE_TAGS.add("header");
		//IGNORE_TAGS.add("footer");
	}
}
